﻿using System;
using System.Collections.Generic;
using BizElements.Core;

namespace BizElements.BusinessLayer
{
    /// <summary>Base class for generated entities that implement <see cref="IDataObject"/> interface.</summary>
    [Serializable]
    public abstract class DataObjectBase : EntityModelBase, IDataObject, ISelfValidatingObject, IValueObject, ICaptionProvider
    {
        #region Constructors.

        /// <summary>Initializes a new instance of DataObjectBase.</summary>
        /// <param name="dbTable">Metadata for table/view to which the entity belongs to.</param>
        /// <remarks>Sets table/view and the catalog to which this entity belongs to.</remarks>
        protected DataObjectBase(IDbTable dbTable)
            : base(dbTable)
        {
        }

        /// <summary>Initializes a new instance of DataObjectBase.</summary>
        /// <param name="table">Metadata for table/view to which the entity belongs to.</param>
        /// <param name="dbTableClass">A class that implements <see cref="IDbTable"/> interface. Must be same as typeof(table) but this is not checked to ensure faster performance.</param>
        /// <param name="entityState">Specifies the state of entity with regard to data-source.</param>
        protected DataObjectBase(IDbTable table, Type dbTableClass, EntityState entityState)
            : base(table, dbTableClass, entityState)
        {
        }

        #endregion

        #region IValueObject.

        private bool isReadOnly = false;

        /// <summary>
        /// Gets or sets the value which indicates whether the data stored in the object may be changed.
        /// </summary>
        public virtual bool IsReadOnly
        {
            get { return this.isReadOnly; }
            set { this.isReadOnly = value; }
        }

        /// <summary>
        /// Sets the given value in the specified property.
        /// </summary>
        /// <param name="propertyName">The name of the property which is to receive the value.</param>
        /// <param name="val">The value to be set. Must be compatibile with the type of the property.</param>
        public virtual void SetPropertyValue(string propertyName, object val)
        {
            OrmUtil.SetPropertyValue(this, propertyName, val);
        }

        #endregion

        #region IsNew.

        /// <summary>
        /// Gets the value which indicates whether the entity is new or existing, ie. retrieved from database.
        /// </summary>
        public virtual bool IsNew
        {
            get
            {
                bool isNew = (this.EntityState == EntityState.New);
                return isNew;
            }
        }

        #endregion

        #region MessageSource.

        [NonSerialized]
        ITextSource messageSource;
        string messageSourceCultureName;        

        /// <summary>Gets a delegate to a method which ensures that the message source is initialized.</summary>
        /// <remarks>Must be implemented in derived classes.</remarks>
        protected abstract TextSourceInitializer EnsureMessageSource { get; }

        /// <summary>Source of messages emitted by a business object. Eg. validation messages, captions.</summary>
        public ITextSource MessageSource
        {
            get
            {
                EnsureMessageSource(ref this.messageSource, ref this.messageSourceCultureName);
                return this.messageSource;
            }
            set
            {
                this.messageSource = value;
                this.messageSourceCultureName = this.messageSource.CurrentCulture.Name;
            }
        }

        #endregion

        #region ICaptionProvider.

        /// <summary>
        /// Gets the string which is prefixes the property name in the text source.
        /// </summary>
        /// <value>A string. Default is the name of the name of the table in the database, ie. the value of the Table.TableName property.</value>
        protected virtual string MessageKeyPrefix
        {
            get { return this._Table.TableName; }
        }

        /// <summary>
        /// Gets the string which delimites the MessageKeyPrefix from the property name in the text source.
        /// </summary>
        /// <value>A string. Default is "_".</value>
        protected virtual string MessageKeyDelimiter
        {
            get { return "_"; }
        }

        /// <summary>
        /// Gets the caption for the specified property.
        /// </summary>
        /// <param name="propertyName">Property. If null or empty then the method will try to return object caption.</param>
        /// <returns>Caption. String.Empty if it doesn't exist in the text source.</returns>
        public virtual string GetFieldCaption(string propertyName)
        {
            string caption;
            if (string.IsNullOrEmpty(propertyName))
                caption = this.MessageSource.GetString(this.MessageKeyPrefix);
            else
                caption = this.MessageSource.GetString(this.MessageKeyPrefix + this.MessageKeyDelimiter + propertyName);

            return (caption != null) ? caption : "";
        }

        /// <summary>
        /// Gets the caption for the specified field.
        /// </summary>
        /// <param name="field">Field. If null then the method will try to return object caption.</param>
        /// <returns>Caption. String.Empty if it doesn't exist in the text source.</returns>
        public virtual string GetFieldCaption(IDbColumn field)
        {
            if (field == null)
                return GetFieldCaption(string.Empty);
            else
                return GetFieldCaption(field.PropertyName);
        }

        #endregion

        #region SetModifiedBy.

        /// <summary>Sets the shaddow/system fields, specified in entity's descriptor returnd by <see cref="GetDataObjectDescriptor()"/> method, which trace who and when modified the database record.</summary>
        /// <param name="actionInitiator">An actor which created or modified the object.</param>
        public virtual void SetModifiedBy(IActor actionInitiator)
        {
            var descriptor = GetDataObjectDescriptor();
            if (this.IsNew)
            {
                IDbColumn createdByField = GetFieldSpecifiedInDescriptor(_Table, descriptor.Fields.CreatedByPropertyName ?? descriptor.Fields.UpdatedByPropertyName);
                if (createdByField != null)
                    SetField(createdByField, actionInitiator.Name);

                IDbColumn createdOnField = GetFieldSpecifiedInDescriptor(_Table, descriptor.Fields.CreatedOnPropertyName ?? descriptor.Fields.UpdatedOnPropertyName);
                if (createdOnField != null)
                    SetField(createdOnField, Time.Now);
            }
            else
            {
                IDbColumn updatedByField = GetFieldSpecifiedInDescriptor(_Table, descriptor.Fields.UpdatedByPropertyName);
                if (updatedByField != null)
                    SetField(updatedByField, actionInitiator.Name);

                IDbColumn updatedOnField = GetFieldSpecifiedInDescriptor(_Table, descriptor.Fields.UpdatedOnPropertyName);
                if (updatedOnField != null)
                    SetField(updatedOnField, Time.Now);
            }
        }

        private static IDbColumn GetFieldSpecifiedInDescriptor(IDbTable table, string propertyName)
        {
            if (string.IsNullOrEmpty(propertyName))
                return null;

            IDbColumn field = table.Columns.GetByPropertyName(propertyName);
            if (field == null)
                throw new InvalidOperationException("The property specified in the entity decriptor does not exist. Property name: " + propertyName);

            return field;
        }

        #endregion

        #region Validate.

        /// <summary>Performs basic field validaton. Override in derived classes to add more advanced validation.</summary>
        /// <param name="level">Specifies the detail of validation.</param>
        /// <returns>Collection of broken rules.</returns>
        public virtual BusinessRuleCollection Validate(ValidationLevel level)
        {
            BusinessRuleCollection rules = OrmUtil.CheckFieldConstraints(this, OrmUtil.CheckFieldConstraintsFlags.ZeroNotAllowedInIntegerFkField);
            return rules.GetBrokenRules();
        }

        #endregion

        /// <summary>Gets object that contains information about weak entity structure, i.e. special fields, and field acces permissions.</summary>
        /// <returns>Weak entity descriptor.</returns>
        public abstract IDataObjectDescriptor GetDataObjectDescriptor();

        #region ConcurrencyTrackingPropertyValues - automatically changed when entity is fetched, updated or inserted.

        IDictionary<string, object> concurrencyTrackingPropertyValues;

        /// <summary>Gets a dictionary to which values of change tracking properties are stored immediately after an object has been fetched or saved.
        /// Used by optimistic offline concurrency control scheme to check if another user or process has changed the object.
        /// Empty if optimistic offline locking is not used, i.e. if tracking properties are not defined in object's descriptor.</summary>
        public IDictionary<string, object> ChangeTrackingPropertyValues
        {
            get
            {
                if (this.concurrencyTrackingPropertyValues == null)
                    this.concurrencyTrackingPropertyValues = new Dictionary<string, object>();

                return this.concurrencyTrackingPropertyValues;
            }
        }

        /// <summary>Automatically sets tracking properties values whenever entity state changes to <b>Synchronized</b>, i.e. imeddiately after immediately after fetch, update or insert operation.</summary>
        public override EntityState EntityState
        {
            get
            {
                return base.EntityState;
            }
            set
            {
                bool hasChanged = (value != this.EntityState);
                base.EntityState = value;
                bool shouldStoreNewTrackingPropertiesValues = hasChanged && (this.EntityState == EntityState.Synchronized) && !ArrayUtil.IsNullOrEmpty(GetDataObjectDescriptor().Fields.ChangeTrackingPropertyNames);
                if (shouldStoreNewTrackingPropertiesValues)
                    LockUtil.SetChangeTrackingPropertiesValues(this);
            }
        }

        #endregion
    }
}
